package com.xiaoyu.net;

import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;

import com.tencent.bugly.crashreport.CrashReport;

import in.srain.cube.util.StringUtil;
import in.srain.cube.util.log.LogEvent;

import com.xiaoyu.net.exception.AccessTokenExpiredException;
import com.xiaoyu.net.log.NetworkLogClientProxy;
import com.xiaoyu.net.progress.ProgressRequestBody;

import org.json.JSONObject;

import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.net.ssl.SSLException;

import in.srain.cube.request.FailData;
import in.srain.cube.request.IRequest;
import in.srain.cube.request.IRequestProxy;
import in.srain.cube.request.JsonData;
import in.srain.cube.request.RequestData;
import in.srain.cube.request.progress.ProgressListener;
import in.srain.cube.util.CLog;
import in.srain.cube.util.NetworkStatusManager;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

final public class RequestProvider {

    private static final String TAG = "RequestProvider";
    public final static int CODE_SUCCESS = 200;
    public final static int CODE_OK = 0;

    static private RequestProvider sInstance;

    private Map<String, Object> mCommonHeaderData;

    static public boolean hasNetwork() {
        NetworkStatusManager manager = NetworkStatusManager.getInstance();
        int type = manager.getNetworkType();
        return manager.isWifi() || type == NetworkStatusManager.NETWORK_CLASS_2G || type == NetworkStatusManager.NETWORK_CLASS_3G || type == NetworkStatusManager.NETWORK_CLASS_4G;
    }

    static synchronized public RequestProvider getInstance() {
        if (null == sInstance) {
            sInstance = new RequestProvider();
        }
        return sInstance;
    }

    public static Map<String, ?> processParams(JSONObject param) {
        Map<String, String> result = null;
        if (null != param) {
            result = new HashMap<>();
            Iterator<String> iterator = param.keys();
            while (iterator.hasNext()) {
                String key = (String) iterator.next();
                String val = param.optString(key);
                result.put(key, val);
            }
        }
        return result;
    }

    public static Map<String, ?> processParams(String... nameAndValue) {
        Map<String, String> result = null;
        if (null != nameAndValue && nameAndValue.length > 0) {
            result = new HashMap<>();
            for (int i = 0; i < nameAndValue.length; i += 2) {
                String key = nameAndValue[i];
                String val = "";
                if (i + 1 < nameAndValue.length) {
                    val = nameAndValue[i + 1];
                }
                if (null != val && !TextUtils.isEmpty(val)) {
                    result.put(key, val);
                }
            }
        }
        return result;
    }

    public static Headers buildHeaders(@Nullable Map<String, Object> commonHeaderData, @Nullable Map<String, Object> singleRequestHeader) {
        Headers.Builder headerBuilder = new Headers.Builder();
        if (commonHeaderData != null) {
            for (Map.Entry<String, Object> entry : commonHeaderData.entrySet()) {
                if (entry.getKey() != null && entry.getValue() != null) {
                    headerBuilder.add(entry.getKey(), entry.getValue().toString());
                }
            }
        }
        if (singleRequestHeader != null) {
            for (Map.Entry<String, Object> entry : singleRequestHeader.entrySet()) {
                if (entry.getKey() != null && entry.getValue() != null) {
                    headerBuilder.add(entry.getKey(), entry.getValue().toString());
                }
            }
        }
        return headerBuilder.build();
    }

    public static <T, OriginDataType> T doSyncRequest(IRequest<T, OriginDataType> cubeRequest, Request request, IRequestProxy<OriginDataType> baseRequestProxy) throws Throwable {
        final LogEvent event = new LogEvent("api");
        event.putData("api", request.url().url().getPath());
        event.putData("commonHeader", JsonData.create(RequestProvider.getInstance().getCommonHeaderData()));
        event.putData("queryData", JsonData.create(cubeRequest.getRequestData().getQueryData()));
        event.putData("postData", JsonData.create(cubeRequest.getRequestData().getPostData()));

        String responseString = StringUtil.blank();
        T data = null;
        Response response = null;
        try {
            final long start = System.currentTimeMillis();
            event.startTimeForKey("requestTime");
            response = OkHttpClientSupplier.get().newCall(request).execute();
            responseString = response.body().string();
            event.endTimeForKey("requestTime");
            event.putData("dataSize", responseString.length());
            event.putData("code", response.code());

            JsonData jsonData = JsonData.create(responseString);
            CLog.v(TAG, "Request: \n CommonHeaders=%s, \n %s \n Response: %s", RequestProvider.getInstance().getCommonHeaderData(), cubeRequest.getRequestData(), jsonData.length() > 0 ? jsonData.toString(4) : responseString);

            event.startTimeForKey("processData");
            final long rt = System.currentTimeMillis() - start;
            final Object t = baseRequestProxy.processResponse(cubeRequest, response.code(), responseString, rt);
            event.endTimeForKey("processData");
            NetworkLogClientProxy.Companion.getInstance().addLog(event);
            if (t == null) {
                if (cubeRequest.getFailData() == null) {
                    cubeRequest.setFailData(FailData.fromNullObject(cubeRequest));
                }
                return null;
            } else {
                data = (T) t;
            }
        } catch (AccessTokenExpiredException ex) {
            ex.printStackTrace();

            event.endTimeForKey("requestTime");
            event.putData("error", "tokenExpired");
            event.putData("detail", Log.getStackTraceString(ex));
            NetworkLogClientProxy.Companion.getInstance().addLog(event);

            CrashReport.postCatchedException(ex);

            throw ex;
        } catch (SSLException | SocketTimeoutException | UnknownHostException | ConnectException e) {
            e.printStackTrace();

            try {
                OkHttpClientSupplier.get().connectionPool().evictAll();
            } catch (Exception e1) {
                e1.printStackTrace();
            }

            String error = "unknown host";
            String detail = e.getMessage();
            if (e instanceof SocketTimeoutException) {
                error = "socket time out";
                detail = Log.getStackTraceString(e);
            } else if (e instanceof ConnectException) {
                error = "connect";
                detail = Log.getStackTraceString(e);
            } else if (e instanceof SSLException) {
                error = "ssl";
                detail = Log.getStackTraceString(e);
            }

            event.endTimeForKey("requestTime");
            event.putData("error", error);
            event.putData("detail", detail);
            NetworkLogClientProxy.Companion.getInstance().addLog(event);

            CrashReport.postCatchedException(e);

            cubeRequest.setFailData(FailData.fromException(cubeRequest, e));
        } catch (Exception e) {
            e.printStackTrace();

            event.endTimeForKey("requestTime");
            event.putData("error", "exception");
            event.putData("detail", Log.getStackTraceString(e));
            NetworkLogClientProxy.Companion.getInstance().addLog(event);

            CrashReport.postCatchedException(e);

            cubeRequest.setFailData(FailData.fromException(cubeRequest, e));
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }

    public Request buildRequest(IRequest cubeRequest) {
        final RequestData requestData = cubeRequest.getRequestData();
        if (requestData.isGetRequest()) {
            return newRequestBuilder(requestData).url(requestData.getRequestUrl()).build();
        } else {
            return buildRequest(requestData);
        }
    }

    private Request.Builder newRequestBuilder(RequestData requestData) {
        Request.Builder builder = new Request.Builder();
        final Map<String, Object> singleRequestHeader = requestData.getHeaderData();

        final Headers headers = buildHeaders(mCommonHeaderData, singleRequestHeader);
        builder.headers(headers);
        return builder;
    }

    public RequestProvider setHeader(String key, Object value) {
        if (mCommonHeaderData == null) {
            mCommonHeaderData = new HashMap<>();
        }
        mCommonHeaderData.put(key, value);
        return this;
    }

    public Map<String, Object> getCommonHeaderData() {
        return Collections.unmodifiableMap(mCommonHeaderData);
    }

    private Request buildRequest(RequestData requestData) {

        final Request.Builder builder = newRequestBuilder(requestData);
        builder.url(requestData.getRequestUrl());

        final String contentType = requestData.getContentType();
        final String bodyContent = requestData.getRequestBody();
        final ProgressListener progressListener = requestData.getProgressListener();
        RequestBody requestBody = null;
        if (!TextUtils.isEmpty(contentType) && !TextUtils.isEmpty(bodyContent)) {
            requestBody = RequestBody.create(MediaType.parse(contentType), bodyContent);
        } else {
            final Map<String, ?> postParam = requestData.getPostData();
            if (requestData.isMultiPart()) {
                MultipartBody.Builder multiPartBodyBuilder = new MultipartBody.Builder();
                multiPartBodyBuilder.setType(MultipartBody.FORM);

                if (postParam != null) {
                    for (Map.Entry<String, ?> pair : postParam.entrySet()) {
                        multiPartBodyBuilder.addFormDataPart(pair.getKey(), pair.getValue().toString());
                    }
                }

                for (Map.Entry<String, RequestData.UploadFileInfo> pair : requestData.getUploadFiles().entrySet()) {
                    RequestData.UploadFileInfo uploadFileInfo = pair.getValue();
                    RequestBody part = RequestBody.create(MediaType.parse("text/plain"), uploadFileInfo.uploadFile);
                    multiPartBodyBuilder.addFormDataPart(uploadFileInfo.fieldName, uploadFileInfo.uploadFile.getAbsolutePath(), part);
                }
                MultipartBody multipartBody = multiPartBodyBuilder.build();
                if (progressListener != null) {
                    requestBody = new ProgressRequestBody(multipartBody, progressListener);
                } else {
                    requestBody = multipartBody;
                }
            } else {
                if (null != postParam) {
                    final FormBody.Builder formBuilder = new FormBody.Builder();
                    for (Map.Entry<String, ?> pair : postParam.entrySet()) {
                        // if the value is null, we should also post out the from name
                        if (pair.getValue() != null) {
                            formBuilder.add(pair.getKey(), pair.getValue().toString());
                        } else {
                            formBuilder.add(pair.getKey(), RequestData.EMPTY);
                        }
                    }
                    requestBody = formBuilder.build();
                }
            }
        }
        final String method = requestData.getMethod();
        if (RequestData.POST.equals(method)) {
            builder.post(requestBody);
        } else if (RequestData.PUT.equals(method)) {
            builder.put(requestBody);
        } else if (RequestData.DELETE.equals(method)) {
            builder.delete(requestBody);
        } else if (RequestData.PATCH.equals(method)) {
            builder.patch(requestBody);
        }
        return builder.build();
    }
}
